Logging, Monitoring & Observabilitas
Ini bukan fitur, ini praktik. Backend produksi tanpa keduanya adalah kotak hitam. Dengan keduanya, Anda bisa menentukan state internal seluruh sistem hanya dengan melihat apa yang ia pancarkan. Itulah definisi sistem yang observable.
Kenapa Observabilitas Penting
Aplikasi backend modern berjalan di lingkungan terdistribusi: banyak service, banyak server, banyak region, pengguna tersebar di seluruh dunia. Sesuatu akan rusak. Pertanyaannya bukan jika, tapi kapan, dan yang lebih penting, seberapa cepat Anda bisa menemukannya dan memperbaikinya?
Tanpa logging, monitoring, dan observabilitas, saat insiden terjadi Anda buta total. Anda tahu sesuatu salah (pengguna mengeluh, konversi turun), tapi tidak tahu apa, di mana, atau kenapa. Setiap menit debugging dalam kegelapan adalah pendapatan hilang dan kepercayaan pengguna terkikis.
Semua ini diimplementasikan pada spektrum: tidak ada perusahaan yang jujur bisa bilang mereka mengikuti 100% semua praktik observabilitas. Tujuannya terus menaikkan cakupan Anda, bukan mencapai semacam state sempurna yang mitis. Jangan gentar oleh banyaknya tool dan praktik di ruang ini.
Praktik ini juga menuntut usaha kolektif. Sebagai developer, Anda meng-instrumentasi kode Anda. Tim DevOps / SRE mengonfigurasi infrastruktur yang mengumpulkan, menyimpan, dan menampilkan data itu. Tak satu sisi pun cukup sendirian.
Tiga Pilar Observabilitas
Teori observabilitas mendefinisikan tiga pilar fondasi. Sistem hanya sepenuhnya observable saat ketiganya ada. Setiap pilar menjawab pertanyaan berbeda tentang perilaku sistem Anda.
Timestamped records of discrete events in your application, a request came in, a database query ran, an error occurred. Think of logs as your backend's diary.
Numerical measurements aggregated over time, request rate, error rate, latency p99, CPU usage. Metrics tell you trends, not individual events.
A trace follows a single request through every component it touched, handler -> service -> DB -> external API. Shows component interactions and latency per step.
Monitoring vs Observabilitas: Perbedaannya
Istilah ini sering dipakai bergantian tapi mereka konsep berbeda di tingkat kemampuan yang berbeda.
| Dimensi | Monitoring | Observabilitas |
|---|---|---|
| Pertanyaan inti | "Ada yang salah?" | "Apa persisnya yang salah dan kenapa?" |
| Pendekatan | Dashboard dan alert yang sudah didefinisikan pada metrik yang dikenal | Investigasi ad-hoc memakai log + metrik + trace secara bersama |
| Cakupan | Melacak metrik yang Anda putuskan diukur sebelumnya | Bisa menjawab pertanyaan yang tidak Anda tahu untuk ditanyakan saat setup |
| Keluaran | "CPU di 95%" -> alert menyala | "CPU 95% karena fungsi X menjalankan query DB N+1 untuk segmen pengguna Y" |
| Analogi historis | Dashboard mobil: kecepatan, bensin, suhu | Sistem diagnostik penuh: memberi tahu komponen dan penyebab yang persis rusak |
| Yang dibutuhkan | Koleksi metrik, aturan alert | Log + Metrik + Trace semuanya terkorelasi dan tertaut |
Satu dekade lalu, monitoring bentuk utama deteksi error. Anda set alert Nagios pada CPU dan memori lalu selesai. Gerakan observabilitas muncul karena di sistem microservice/terdistribusi, mengetahui bahwa sesuatu gagal hampir sia-sia tanpa mengetahui di mana di antara lusinan service kegagalan itu sebenarnya berasal.
Wawasan kunci: Monitoring memberi tahu Anda ada masalah. Observabilitas memberi tahu Anda persis apa masalahnya, fungsi spesifik, service spesifik, pengguna spesifik, query spesifik. Ini hanya mungkin jika Anda mengimplementasikan ketiga pilar dengan benar.
Cara Ketiganya Bekerja Bersama: Alur Kerja Debugging
Dalam praktik, saat insiden terjadi, Anda menavigasi dari sinyal berbutir kasar ke detail berbutir halus. Inilah alur kanonis yang dipakai sistem produksi:
- 1. Alert menyala (Monitoring). Grafana / New Relic mendeteksi tingkat error > 80% dan menembakkan webhook Slack. Anda dapat pesan: "tingkat error service API melonjak di 14:32." Ini monitoring melakukan tugasnya.
- 2. Pergi ke dashboard Metrik. Buka Grafana / New Relic. Anda melihat angka konkret: 840 request/menit, 82% mengembalikan 5xx, latensi p99 melonjak dari 120ms ke 4,2 detik di 14:31. Anda kini tahu skala masalahnya dan kapan ia mulai.
- 3. Korelasikan ke Log. Dari tampilan metrik, klik ke log error terkait untuk jendela 2 menit itu. Anda melihat ratusan log:
ERROR: connection pool exhausted after 4000ms. Kini Anda tahu apa yang gagal. - 4. Ikuti Trace. Klik salah satu log error itu. Ia menaut ke trace. Trace menampilkan perjalanan lengkap request:
AuthMiddleware (2ms) -> UserHandler (5ms) -> UserService (3ms) -> DB.GetUser (3990ms <- DI SINI). Query DB-nya timeout. Akar masalah ditemukan. - 5. Perbaiki & Konfirmasi dengan Metrik. Anda menambah index yang hilang ke query. Dalam hitungan menit, dashboard Grafana menunjukkan tingkat error turun kembali ke 0,2%, latensi kembali ke 110ms. Monitoring mengonfirmasi perbaikan bekerja.
Alert -> Metrik -> Log -> Trace hanya mungkin saat ketiga pilar terkorelasi. Tool seperti Grafana, New Relic, dan Datadog menautkannya memakai traceid / requestid bersama yang mengalir lewat setiap komponen. Inilah kenapa men-set request ID di middleware dan meneruskannya ke mana-mana itu kritis.
Logging: Selami Mendalam
Logging adalah praktik mencatat semua peristiwa penting di sepanjang siklus eksekusi aplikasi Anda. Anggap ia jurnal yang dipelihara backend Anda, entri ber-timestamp, terperinci, terstruktur untuk setiap hal signifikan yang terjadi.
Apa yang Harus Anda Log?
Aturan praktis yang baik: apa pun yang ingin diketahui developer saat men-debug masalah jam 2 pagi, tanpa akses ke sistem live, tanpa kemampuan mereproduksi bug, hanya bermodal log.
Metadata Apa yang Disertakan di Setiap Log
Log tanpa konteks hampir sia-sia. Setiap entri log harus membawa metadata yang cukup untuk menjawab: siapa melakukan apa, kapan, di mana, dan dengan hasil apa?
// A well-structured log entry (JSON, production format)
{
"timestamp": "2024-01-15T14:32:01.234Z",
"level": "error",
"message": "database query failed",
"service": "todo-api",
"environment": "production",
"request_id": "req_01J2KXYZ", // unique per request
"trace_id": "abc123def456", // links to distributed trace
"span_id": "span_789",
"user_id": "usr_01J2K",
"method": "POST",
"path": "/todos",
"error": "pq: deadlock detected",
"duration_ms": 4001,
"host": "worker-node-3"
}
JSON Log Entry
Log Level
Setiap entri log diberi tingkat keparahan. Ini membuat Anda bisa memfilter apa yang dilihat, di development Anda ingin semuanya, di produksi Anda peduli warning ke atas. Library logging Anda akan membiarkan Anda men-set level minimum per lingkungan.
Level membentuk hirarki: DEBUG < INFO < WARN < ERROR < FATAL. Men-set logger ke INFO berarti ia memancarkan INFO, WARN, ERROR, dan FATAL, tapi menekan DEBUG. Men-set ke ERROR berarti hanya ERROR dan FATAL yang dipancarkan. Begitulah Anda mengendalikan volume log per lingkungan.
Konfigurasi Level Khusus Lingkungan (Go)
// logger/logger.go, Environment-aware log level
package logger
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func getLogLevel() zapcore.Level {
env := os.Getenv("APP_ENV")
switch env {
case "production":
return zapcore.InfoLevel // production: INFO and above
case "staging":
return zapcore.WarnLevel // staging: WARN and above only
default:
return zapcore.DebugLevel // local dev: everything
}
}
func New() *zap.Logger {
env := os.Getenv("APP_ENV")
level := getLogLevel()
var cfg zap.Config
if env == "production" {
// Production: JSON format, parseable by Loki, ELK, New Relic
cfg = zap.NewProductionConfig()
cfg.EncoderConfig.TimeKey = "timestamp"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
} else {
// Development: coloured console format, human-readable
cfg = zap.NewDevelopmentConfig()
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
cfg.Level = zap.NewAtomicLevelAt(level)
logger, _ := cfg.Build()
return logger
}
Go / Zap
Logging Terstruktur vs Tidak Terstruktur
Ada dua cara fundamental menulis entri log. Pilihan yang mana tergantung lingkungan Anda.
| Sifat | Tidak Terstruktur (Teks Polos) | Terstruktur (JSON) |
|---|---|---|
| Format | 2024-01-15 14:32 ERROR: DB query failed for user 42 |
{"level":"error","user_id":"42","error":"pq: timeout",...} |
| Keterbacaan manusia | Mudah dibaca di terminal | Lebih sulit dibaca tanpa viewer |
| Keteruraian mesin | Sulit, butuh regex untuk mengekstrak user_id, pesan error, dll. | JSON asli, tool apa pun bisa mem-parse seketika |
| Ekstraksi field | Pola regex rawan error | Akses field langsung: log.user_id |
| Query di Loki/ELK | Kompleks, pola grep rapuh | {user_id="42"} |= "error", sepele |
| Dipakai di | Pengembangan lokal | Staging & Produksi |
Seperti Apa Log Terstruktur Berdampingan
// UNSTRUCTURED, development console (human-friendly)
14:32:01 DEBUG Connected to PostgreSQL on localhost:5432
14:32:01 INFO Started background job worker
14:32:01 INFO HTTP server listening on :8080
14:32:14 WARN Slow query detected: 520ms query=SELECT * FROM todos WHERE user_id=$1
14:32:15 ERROR Failed to send email user_id=usr_01J2K error=connection refused
Console (Dev)
// STRUCTURED, production JSON (machine-friendly)
{"timestamp":"2024-01-15T14:32:14Z","level":"warn","msg":"slow query","duration_ms":520,"query":"SELECT * FROM todos WHERE user_id=$1","service":"todo-api","env":"production"}
{"timestamp":"2024-01-15T14:32:15Z","level":"error","msg":"failed to send email","user_id":"usr_01J2K","error":"connection refused","trace_id":"abc123","span_id":"span_001","service":"todo-api","env":"production"}
JSON (Production)
Sistem manajemen log (Loki, ELK, New Relic, Datadog) menelan ribuan baris log per detik. Jika log teks polos, tool ini harus memakai regex untuk mengekstrak field seperti userid, error, durationms, yang lambat, rawan error, dan rapuh. JSON membuat mereka mem-parse field secara natif dalam O(1). Selalu pakai JSON di produksi.
Metrik: Selami Mendalam
Metrik adalah pengukuran numerik perilaku sistem Anda, digabungkan seiring waktu. Tidak seperti log (satu entri per peristiwa), metrik adalah counter atau gauge yang merangkum banyak peristiwa menjadi satu angka, mis. "320 request dalam 15 detik terakhir". Ini membuatnya sangat efisien untuk dashboard dan alerting.
Empat Tipe Metrik Inti (Model Prometheus)
rate(http_requests_total[5m]) = requests/sec over last 5 min.Metrik Kunci yang Harus Diekspos Setiap Backend
| Metrik | Tipe | Kenapa Penting |
|---|---|---|
httprequeststotal | Counter | Throughput request. Label: method, path, status_code. |
httprequestduration_seconds | Histogram | Distribusi latensi. p99 memberi tahu pengalaman pengguna kasus terburuk. |
httperrorstotal | Counter | Jumlah respons 4xx / 5xx. Lonjakan = ada yang rusak. |
dbqueryduration_seconds | Histogram | Query DB lambat. Lonjakan menandakan index hilang atau masalah N+1. |
dbconnectionsopen | Gauge | Risiko kehabisan pool. Mendekati maks = Anda butuh koneksi lebih atau query lebih sedikit. |
taskqueuedepth | Gauge | Tumpukan kerja di sistem background task Anda. Naik = worker perlu scaling. |
taskprocessingduration_seconds | Histogram | Berapa lama background task berjalan. Berguna untuk pelacakan SLA. |
gogoroutines / processresidentmemorybytes | Gauge | Kesehatan runtime. Kebocoran goroutine = ini terus naik. |
Interval Scrape & Jeda
Prometheus men-scrape endpoint /metrics Anda pada interval terkonfigurasi (biasanya 15 detik). Ini berarti ada jeda bawaan ±10-15 detik antara kenyataan dan yang Anda lihat di dashboard. Ini bisa diterima untuk kebanyakan kasus. Untuk alerting yang mendekati real-time, pakai interval scrape di bawah 15 detik, tapi sadari ini menaikkan beban di service Anda dan Prometheus.
Distributed Tracing
Trace adalah catatan lengkap perjalanan satu request melewati sistem Anda. Setiap langkah perjalanan itu disebut span. Span ditautkan oleh trace_id bersama yang dibuat saat request pertama masuk ke sistem Anda dan disebarkan lewat setiap panggilan berikutnya.
Trace vs Span: Definisi
trace_id. Contains all spans for that request.trace_id and span_id are passed from function to function (via Go's context.Context), and from service to service (via HTTP headers like traceparent).traceparent: 00-abc123-span001-01. When your service calls another service, it includes this header so the receiving service can join its spans to the same trace.Entri log berbunyi "error ini terjadi". Trace berbunyi "error ini terjadi di sini, di titik ini dalam perjalanan request, setelah langkah-langkah pendahulu ini, dan ia menyebabkan total request memakan 4,2 detik alih-alih 50ms". Trace memberi konteks kausal yang tidak bisa diberikan log terisolasi.
Instrumentasi & OpenTelemetry
Instrumentasi adalah tindakan menambah kode ke aplikasi Anda untuk mengukur perilakunya, mencatat span, memancarkan metrik, memperkaya log dengan konteks. Anda tidak bisa mengamati yang tidak Anda instrumentasi.
OpenTelemetry (OTel) adalah standar instrumentasi open source dan netral-vendor. Ia menyediakan API, SDK, dan agen (the Collector) untuk semua bahasa utama. Nilai kuncinya: instrumentasi sekali, kirim ke backend mana pun (Jaeger, Grafana Tempo, New Relic, Datadog, Honeycomb) cukup dengan mengubah konfigurasi, tanpa perubahan kode.
Konsep Inti OTel
otel.Tracer("todo-service")traceparent headers in HTTP clients and servers (with the right libraries). Zero manual work for standard HTTP.Kode: Go: Implementasi LMO Lengkap
Contoh lengkap yang menunjukkan logging (Zap), metrik (Prometheus), dan tracing (OpenTelemetry) dirangkai ke handler service Go. Ini pola dari aplikasi to-do di kuliah.
Setup Logger (Zap)
// logger/logger.go
package logger
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Log *zap.Logger
func Init() {
env := os.Getenv("APP_ENV")
var cfg zap.Config
if env == "production" {
cfg = zap.NewProductionConfig() // JSON output
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
} else {
cfg = zap.NewDevelopmentConfig() // coloured console
cfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
Log, _ = cfg.Build(
// Always include these fields in every log entry
zap.Fields(
zap.String("service", "todo-api"),
zap.String("env", env),
),
)
}
Go / Zap
Tracing Middleware (OpenTelemetry)
// middleware/tracing.go, Creates a trace span per request
package middleware
import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.uber.org/zap"
"myapp/logger"
)
func EnhancedTracing() gin.HandlerFunc {
tracer := otel.Tracer("todo-api")
return func(c *gin.Context) {
// Extract incoming trace context (from load balancer / front-end)
ctx := otel.GetTextMapPropagator().Extract(
c.Request.Context(),
propagation.HeaderCarrier(c.Request.Header),
)
// Start root span for this request
ctx, span := tracer.Start(ctx, c.FullPath())
defer span.End()
// Attach metadata to span (visible in trace UI)
span.SetAttributes(
attribute.String("http.method", c.Request.Method),
attribute.String("http.path", c.FullPath()),
attribute.String("http.user_agent", c.Request.UserAgent()),
attribute.String("user.id", getUserID(c)),
)
// Inject context so downstream handlers can create child spans
c.Request = c.Request.WithContext(ctx)
c.Next()
// After handler returns: record status code on span
statusCode := c.Writer.Status()
span.SetAttributes(attribute.Int("http.status_code", statusCode))
if statusCode >= 500 {
span.SetStatus(codes.Error, "server error")
}
// Structured log entry linking to this trace
logger.Log.Info("request completed",
zap.String("method", c.Request.Method),
zap.String("path", c.FullPath()),
zap.Int("status", statusCode),
zap.String("trace_id", span.SpanContext().TraceID().String()),
)
}
}
Go / Gin + OTel
Service Layer: Logging + Tracing + Penanganan Error
// services/todo_service.go, The full LMO pattern per function
package services
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.uber.org/zap"
"myapp/logger"
"myapp/models"
)
var tracer = otel.Tracer("todo-service")
func CreateTodo(ctx context.Context, req models.CreateTodoRequest, userID string) (*models.Todo, error) {
// 1. Start a child span (parent span is in ctx from the middleware)
ctx, span := tracer.Start(ctx, "TodoService.CreateTodo")
defer span.End()
// 2. Add business context to the span, visible in trace UI
span.SetAttributes(
attribute.String("user.id", userID),
attribute.String("todo.title", req.Title),
attribute.String("todo.priority", req.Priority),
)
// 3. INFO log, record business event start
logger.Log.Info("creating todo",
zap.String("user_id", userID),
zap.String("title", req.Title),
zap.String("trace_id", span.SpanContext().TraceID().String()),
)
// 4. Execute DB operation (inside its own child span, auto via otelgorm)
todo, err := repo.CreateTodo(ctx, req, userID)
if err != nil {
// ERROR log with full context
logger.Log.Error("failed to create todo",
zap.String("user_id", userID),
zap.String("error", err.Error()),
zap.String("trace_id", span.SpanContext().TraceID().String()),
)
// Record error on span, shows as failed in Jaeger/Grafana Tempo
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, fmt.Errorf("repo.CreateTodo: %w", err)
}
// 5. DEBUG log, only visible in dev, suppressed in production
logger.Log.Debug("todo created",
zap.String("todo_id", todo.ID),
)
// 6. INFO, business event completion log (for audit trail)
logger.Log.Info("todo created successfully",
zap.String("todo_id", todo.ID),
zap.String("user_id", userID),
zap.String("title", todo.Title),
zap.String("priority", todo.Priority),
zap.String("category_id", todo.CategoryID),
)
return todo, nil
}
Go / Zap + OTel
Prometheus Middleware (Metrik)
// middleware/metrics.go, HTTP metrics instrumentation
package middleware
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
}, []string{"method", "path", "status"})
httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
}, []string{"method", "path"})
)
func PrometheusMetrics() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
status := strconv.Itoa(c.Writer.Status())
httpRequestsTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
httpDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration.Seconds())
}
}
Go / Gin + Prometheus
Kode: Python: Implementasi LMO Lengkap
Structured Logging dengan structlog
# logging_config.py, structlog setup for Python
import logging
import os
import structlog
def configure_logging():
env = os.getenv("APP_ENV", "development")
if env == "production":
# JSON renderer for production, parseable by Loki / ELK
renderer = structlog.processors.JSONRenderer()
level = logging.INFO
else:
# ColourfulConsole for local dev, human-readable
renderer = structlog.dev.ConsoleRenderer(colors=True)
level = logging.DEBUG
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars, # merge request-scoped context
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
structlog.processors.StackInfoRenderer(),
structlog.processors.ExceptionRenderer(), # auto-renders exceptions
renderer,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logging.basicConfig(level=level)
log = structlog.get_logger()
Python / structlog
OpenTelemetry Tracing di FastAPI
# tracing.py, OTel setup for FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
def setup_tracing(app):
# Tracer provider, sends spans to OTel Collector via gRPC
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Auto-instrument FastAPI, all routes get spans automatically
FastAPIInstrumentor.instrument_app(app)
# Auto-instrument SQLAlchemy, all DB queries get spans automatically
SQLAlchemyInstrumentor().instrument(engine=engine)
# main.py, wire it all up
from fastapi import FastAPI
from tracing import setup_tracing
from logging_config import configure_logging, log
configure_logging()
app = FastAPI()
setup_tracing(app)
Python / FastAPI + OTel
Service Layer dengan Pola LMO Lengkap
# services/todo_service.py
from opentelemetry import trace
import structlog
tracer = trace.get_tracer("todo-service")
log = structlog.get_logger()
async def create_todo(user_id: str, req: CreateTodoRequest) -> Todo:
# 1. Start child span
with tracer.start_as_current_span("TodoService.create_todo") as span:
span.set_attribute("user.id", user_id)
span.set_attribute("todo.title", req.title)
# 2. INFO log, business event start
log.info("creating_todo", user_id=user_id, title=req.title)
try:
todo = await repo.create_todo(user_id, req)
# 3. Business event log, audit trail
log.info("todo_created",
todo_id=todo.id,
user_id=user_id,
title=todo.title,
priority=todo.priority,
)
return todo
except Exception as e:
# 4. ERROR log + span error recording
log.error("todo_creation_failed", user_id=user_id, error=str(e))
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR, str(e))
raise
Python / FastAPI + OTel + structlog
Tooling: Stack Open Source
Stack observabilitas open source adalah yang dijalankan kebanyakan perusahaan besar. Lebih banyak kendali, tanpa vendor lock-in, tanpa harga per-seat, tapi butuh keahlian operasional untuk memeliharanya.
| Tool | Pilar | Peran |
|---|---|---|
| Prometheus | Metrik | Men-scrape endpoint /metrics setiap 15 detik, menyimpan data time-series, mengevaluasi aturan alert. Backend metrik standar de facto. |
| Grafana | Dashboard | Lapisan visualisasi. Meng-query Prometheus, Loki, Tempo, dan lainnya. Bangun dashboard, set ambang alert, buat runbook on-call. |
| Loki | Log | Sistem agregasi log oleh Grafana Labs. Tidak seperti ELK, ia hanya meng-index metadata (label), bukan full-text, jauh lebih murah. Bisa di-query via LogQL. Push-based lewat agen Promtail. |
| Promtail | Log (agen) | Berjalan di tiap server, men-tail file log atau stream log Docker/Kubernetes, lalu mengirimkannya ke Loki. Tanpa perubahan kode sama sekali. |
| Grafana Tempo | Trace | Backend distributed tracing. Menerima span OTel, menyimpannya murah (object storage), bisa di-query dari Grafana. Menggantikan Jaeger untuk setup baru. |
| Jaeger | Trace | Sistem tracing terdistribusi yang lebih tua (CNCF). Masih banyak dipakai. UI untuk menavigasi trace, grafik dependensi service. Kini merekomendasikan migrasi ke SDK OTel. |
| Alertmanager | Alerting | Merutekan alert Prometheus ke Slack, PagerDuty, email, dll. Menangani deduplikasi, pengelompokan, dan peredaman alert. |
| OTel Collector | Ketiganya | Menerima semua telemetri dari aplikasi, memproses/memperkayanya, dan menyebar ke Prometheus + Loki + Tempo. Pipeline telemetri terpusat. |
Arsitektur Stack Lengkap
Tooling: Solusi Proprietary
Jika Anda tidak punya tim untuk mengelola stack open source, solusi all-in-one proprietary pilihan pragmatis. Mereka menggabungkan logging, metrik, tracing, dan alerting ke satu platform dengan penyimpanan terkelola dan UI yang rapi.
| Tool | Kekuatan | Terbaik untuk | Model Harga |
|---|---|---|---|
| New Relic | Observabilitas full-stack, agen Go/Python/Node, APM, monitoring browser, mobile. Setup 10 menit yang mudah. | Tim yang mau satu tool dengan overhead ops minimal | Tier gratis (100GB/bulan), lalu per GB yang ditelan |
| Datadog | Dashboard terdepan industri, 500+ integrasi, APM sangat baik, monitoring infra, keamanan. Sangat kuat. | Perusahaan dengan infra kompleks, Kubernetes, multi-cloud | Per host + per GB, bisa cepat mahal |
| Sentry | Pelacakan error kelas terbaik. Mengelompokkan error identik, menampilkan stack trace, konteks pengguna, pelacakan rilis. | Monitoring error secara khusus, berpasangan baik dengan Grafana untuk metrik | Tier gratis (5k error/bulan), lalu per peristiwa |
| Honeycomb | Query peristiwa high-cardinality. Dibangun untuk tim observability-first. Dukungan OTel natif. | Tim yang melakukan distributed tracing lanjutan, banyak microservice | Per peristiwa yang ditelan |
New Relic Go Agent: Integrasi Cepat
// main.go, New Relic agent setup in Go
import (
"github.com/newrelic/go-agent/v3/newrelic"
"github.com/newrelic/go-agent/v3/integrations/nrgin"
)
func main() {
// Initialize New Relic application agent
nrApp, _ := newrelic.NewApplication(
newrelic.ConfigAppName("todo-api"),
newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
newrelic.ConfigAppLogForwardingEnabled(true), // forward logs to NR
newrelic.ConfigDistributedTracerEnabled(true), // enable distributed tracing
)
r := gin.New()
// nrgin middleware, auto-instruments every route with NR transactions
r.Use(nrgin.Middleware(nrApp))
r.Run(":8080")
}
Go / New Relic
Setelah menambah middleware, New Relic otomatis menangkap: waktu transaksi per route, tingkat error, throughput (req/detik), skor Apdex, metrik runtime Go (jeda GC, jumlah goroutine, memori), dan trace terdistribusi. Dashboard menampilkan semua ini dalam hitungan menit setelah deploy.
ELK Stack: Elasticsearch, Logstash, Kibana
ELK Stack (kini disebut Elastic Stack) adalah platform logging enterprise klasik. Ia menyediakan full-text search lintas log, agregasi kuat, dan dashboard kaya. Lebih berat daripada Loki tapi menawarkan fleksibilitas query lebih banyak.
ELK vs Grafana Loki
| Sifat | ELK Stack | Grafana Loki |
|---|---|---|
| Indexing | Full-text meng-index setiap field -> query kuat | Hanya meng-index label (metadata) -> penyimpanan lebih murah |
| Biaya penyimpanan | Tinggi, meng-index semua field mahal | Rendah, seperti Prometheus untuk log |
| Daya query | Sangat tinggi, KQL, agregasi kompleks | Bagus untuk query berbasis label (LogQL) |
| Integrasi | UI Kibana yang mandiri | Grafana, satu UI untuk metrik + log + trace |
| Terbaik untuk | Kompleksitas query tinggi, kasus keamanan/kepatuhan | Setup yang sensitif biaya dan sudah memakai stack Grafana |
Desain Alert: Apa yang Di-alert
Tidak setiap metrik butuh alert. Alert fatigue masalah nyata, jika engineer menerima terlalu banyak notifikasi, mereka mulai mengabaikannya. Desain alert yang baik berarti alert pada gejala yang berdampak pada pengguna, bukan detail implementasi internal.
Empat Golden Signal (Buku Google SRE)
Waktu melayani sebuah request. Bedakan request sukses dan gagal, error yang cepat bukan metrik yang baik. Alert pada p99 > ambang.
Permintaan pada sistem Anda, request/detik. Penurunan mendadak bisa menandakan pemadaman sama seperti lonjakan mendadak. Alert pada kedua ekstrem.
Tingkat request gagal (5xx). Alert pada tingkat error, bukan jumlah mentah, lonjakan lalu lintas secara alami menyebabkan lebih banyak error bahkan jika tingkat errornya stabil.
Seberapa "penuh" service Anda, CPU, memori, koneksi DB, kedalaman antrean. Alert sebelum sistem terjungkal ke kegagalan. Alert di 80%, page di 95%.
Aturan Alert (format Grafana / Prometheus)
# alert_rules.yml, Production-grade alert rules
groups:
- name: api_health
rules:
# P1, Error rate critical
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m])
/ rate(http_requests_total[5m]) * 100 > 5
for: 2m
labels:
severity: critical
annotations:
summary: "HTTP error rate above 5% ({{ $value | printf \"%.1f\" }}%)"
runbook: "https://wiki.company.com/runbooks/high-error-rate"
# P2, High latency
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "P99 latency above 2s ({{ $value | printf \"%.2f\" }}s)"
# P2, DB connection pool near exhaustion
- alert: DBConnectionPoolExhaustion
expr: db_connections_open / db_connections_max > 0.85
for: 3m
labels:
severity: warning
annotations:
summary: "DB connection pool at {{ $value | humanizePercentage }}"
# P1, Service is down
- alert: ServiceDown
expr: up{job="todo-api"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "todo-api service is unreachable"
YAML / Prometheus Rules
Definisikan setidaknya dua tingkat: warning (selidiki saat nyaman, jam kerja) dan critical (page on-call seketika, jam apa pun). Membangunkan engineer jam 3 pagi untuk masalah tingkat warning adalah cara membakar tim Anda dan mengajari mereka mengabaikan alert.
Alur Kerja Debugging Produksi: Contoh Lengkap
Inilah alur kerja lengkap ujung-ke-ujung saat insiden terjadi di sistem dengan LMO penuh terpasang.
Skenario: API Tiba-tiba Lambat + Banyak Error
- 1. Alert menyala di 14:31 UTC. Alertmanager mengirim pesan Slack: "[CRITICAL] HighErrorRate: HTTP error rate is 82.4% (was 0.3%). Service: todo-api. Runbook: wiki/..."
- 2. Buka Grafana -> dashboard Metrik. Latensi P99 melonjak dari 120ms ke 4,3 detik di 14:31. Requests/detik tidak berubah (bukan lonjakan lalu lintas). Connection pool DB di 97%. Jenis error: 100% 500-an, bukan 4xx. Menandakan masalah DB sisi server.
- 3. Korelasikan ke Log di Loki / New Relic. Query:
{service="todo-api", level="error"} |= "connection". Ratusan entri:ERROR: pq: remaining connection slots are reserved for non-replication superuser connections. Connection pool DB habis. - 4. Lompat ke Trace dari entri log. Klik tautan trace di log. Trace menampilkan: HTTP Handler -> AuthMiddleware (3ms) -> TodoService (4290ms) -> DB.GetTodos (4285ms <- MACET). Span DB.GetTodos punya atribut:
query="SELECT * FROM todos WHERE user_id=$1". - 5. Akar masalah teridentifikasi. Deployment 5 menit lalu menambah
SELECT *yang melakukan full table scan (index hilang). Di bawah beban, query menumpuk, koneksi dipegang lebih lama, pool habis. Cekgit log, mengonfirmasi deployment di 14:29 UTC. - 6. Perbaiki + verifikasi. Tambah migrasi index yang hilang. Deploy. Grafana menunjukkan tingkat error turun ke 0,1% dalam 2 menit setelah deployment. Connection pool DB kembali ke 12%. Insiden selesai. Total waktu: 11 menit.
Insiden yang sama tanpa observabilitas: pengguna mengeluh di Twitter, Anda SSH ke server menjalankan grep ERROR /var/log/app.log, mengkorelasikan timestamp manual, akhirnya menyadari ada yang salah dengan DB, menghabiskan 90 menit mempersempit query mana, lalu menebak soal index yang hilang. Total waktu: minimal 2-3 jam.
Praktik Terbaik
1. Selalu Sertakan Request ID
Buat requestid unik (UUID atau ULID) di titik masuk setiap request, entah di load balancer Anda atau di middleware pertama. Suntikkan ke konteks dan sertakan di setiap_ entri log dan span untuk request itu. Inilah yang membuat Anda bisa menarik semua log untuk satu request yang gagal dari jutaan entri.
2. Log di Batas, Bukan di Dalam Logika
Log di masuk dan keluar komponen utama (handler, service, repository), bukan tersebar di setiap loop dan kondisi. Ini memberi jejak bersih dan terbaca tanpa kebisingan. Di dalam fungsi, pakai span untuk timing granular, bukan spam log.Debug.
3. Jangan Pernah Log Data Sensitif
Kata sandi, nomor kartu kredit, SSN, token auth, dan API key tidak boleh pernah muncul di log. Pakai library logging terstruktur yang memungkinkan Anda menandai field sebagai ter-redact. Di pipeline CI Anda, pertimbangkan langkah pemindai log yang menggagalkan build jika pola tertentu ditemukan di pernyataan log.
4. Pakai Sampling untuk Trace Volume Tinggi
Di lalu lintas tinggi (1000+ req/detik), mencatat setiap trace mahal. Pakai head-based sampling (catat 10% semua request) atau tail-based sampling (catat 100% request error + 1% request sukses). OTel Collector mendukung keduanya. Jangan pernah sampling error, selalu catat trace untuk kegagalan.
5. Disiplin Log Level
- Jangan pernah memakai DEBUG di produksi, ia menghasilkan volume raksasa dan biaya penyimpanan
- INFO harus default produksi Anda, peristiwa bermakna, bukan cerewet
- Perlakukan setiap log ERROR sebagai sesuatu yang butuh tiket atau aturan alert
- FATAL harus berarti "aku butuh seseorang di-page sekarang juga"
6. Ini Spektrum, Bukan Kotak Centang
Mulai dengan logging terstruktur dasar dalam format JSON + satu counter Prometheus sederhana untuk tingkat error. Itu saja menghemat berjam-jam saat insiden produksi pertama terjadi. Lalu tambah secara iteratif: metrik histogram, tracing dasar, dashboard Grafana, aturan alert. Anda tidak butuh stack ELK + Jaeger + OTel Collector penuh di hari pertama. Bangun secara bertahap.
Log memberi tahu Anda apa yang terjadi. Metrik memberi tahu pola dan tren. Trace memberi tahu di mana persis hal-hal salah dan berapa lama tiap langkah. Sistem observable saat ketiganya terpasang dan terkorelasi. Ini butuh usaha developer (instrumentasi kode Anda) dan usaha infrastruktur (kumpulkan, simpan, tampilkan) bekerja bersama. Tooling-nya, entah stack Grafana atau New Relic, sekunder dari praktik meng-instrumentasi kode Anda secara konsisten dengan konteks yang tepat.